module Sortable
  
    module ClassMethods
      ITEMS_PER_PAGE = 25
      
      def search_objects(objects, params, sort_map, include_rel, default_sort, search_map, items_per_page=ITEMS_PER_PAGE)
        conditions = process_search(params, search_map)      
        get_sorted_objects(objects, params, sort_map, include_rel, default_sort, conditions, items_per_page)                 
      end
      
      # Users can also pass in optional conditions that are used by the finder method call. For example if only wanted to
      # show the items that had a certain status value you could pass in a condition 'mytable.status == 300' for example
      # as the conditions parameter and when the finder is called the sortable table will only display objects that meet those
      # conditions. Additionally you can paginate and sort the objects that are returned and apply the conditions to them.
      def get_sorted_objects(objects, params, sort_map, include_rel, default_sort, conditions=nil, items_per_page=ITEMS_PER_PAGE)
        @sort_map = sort_map
        sort = process_sort(params, sort_map, default_sort)
        # fetch the objects, paginated and sorted as desired along with any extra filtering conditions
        get_paginated_objects(objects, sort, include_rel, conditions, params['page'], items_per_page)
      end
      
      private
      def get_paginated_objects(objects, sort, include_rel, conditions, page, items_per_page)
        offset = 0
        if page
          offset = ((page.to_i-1) * items_per_page)
        end

        @objects = objects.find(:all, 
                                 :include => include_rel, 
                                 :order => sort, 
                                 :limit => items_per_page, 
                                 :offset => offset, 
                                 :conditions => conditions)    
        @total_object_count = objects.count(:include => include_rel, :conditions => conditions)
        @first_object = @total_object_count > 0 ? offset + 1 : 0
        @last_object = offset + items_per_page
        @last_object = @total_object_count if @last_object > @total_object_count

        @object_paginator = ActionController::Pagination::Paginator.new self, @total_object_count, items_per_page, page        
      end

      # The search mechanism takes a search map which is a key to an array of DB table columns to search for the given key.
      # The user specifies the key in the select box and the corresponding conditions for the finder query are built here.
      def process_search(params, search_map)
        conditions = ''
        if value_provided?(params, :query) &&
           value_provided?(params, :query_field)
          field = params[:query_field]
          if search_map[field]
            columns_to_search = ''
            values = Array.new        
            g = Generator.new(search_map[field])
            g.each do |col|
              columns_to_search += col + ' LIKE ? '
              columns_to_search += 'OR ' unless g.end?
              values<< "%#{params[:query]}%"
            end
            conditions = [columns_to_search] + values unless params[:query].nil?
          end
        end
        if conditions.empty?
          conditions = nil # don't provide the find method with any search conditions
        end
        return conditions
      end

      def process_sort(params, sort_map, default_sort)
        if params['sort']
          sort = process_sort_param(params['sort'], sort_map)  
        else
          # fetch the table.column from the sortmap for the given sort key and append the sort direction
          sort = sort_map[default_sort[0]][0] + ' ' + default_sort[1]            
          
          # NOTICE these variables are used in the sort_link_helper and sort_td_class_helper to build the column link headings
          # and create the proper CSS class for the column heading for the case where there is no sort param.
          @default_sort = default_sort[0]
          if default_sort[1] && default_sort[1] == 'DESC'
            @default_sort_key = default_sort[0] + '_reverse'
            @sortclass = 'sortup'       
          else
            @default_sort_key = default_sort[0]             
            @sortclass = 'sortdown'
          end
        end      
        return sort    
      end

      def process_sort_param(sort, sort_map)
        mapKey = get_sort_key(sort)
        if mapKey.nil? 
          mapKey = sort
        end
        
        result = ''
        sort_array = sort_map[mapKey]
        # this adds support for more than one sort criteria for a given column
        # for example, status DESC, created_at ASC
        if sort_array[0].class == Array
          g = Generator.new(sort_array)
          g.each do |sort_value|
            column = sort_value[0]
            direction = sort_value[1]
            if /_reverse$/.match(sort)
              if direction == 'DESC'
                direction = 'ASC'
              else
                direction = 'DESC'
              end
            end
            result += column + ' ' + direction
            result += ', ' unless g.end?
          end
        else
          column = sort_array[0]
          direction = sort_array[1]
          if /_reverse$/.match(sort)
            if direction == 'DESC'
              direction = 'ASC'
            else
              direction = 'DESC'
            end
          end
          result = column + ' ' + direction
        end
        
        return result
      end
    end
    
    module SortableHelper
      def sort_td_class_helper(param)
        result = 'sortup' if params[:sort] == param
        result = 'sortdown' if params[:sort] == param + "_reverse"
        result = @sortclass if @default_sort && @default_sort == param    
        return result
      end

      def get_sort_key(sort)
        i = sort.index('_reverse')
        if i
          mapKey = sort[0, i]
        else
          mapKey = sort
        end
        return mapKey
      end

      def sort_link_helper(action, text, param, params, extra_params={})
        options = build_url_params(action, param, params, extra_params)
        html_options = {
          :title => "Sort by this field"
        }        
        link_to(text, options, html_options)
      end

      def build_url_params(action, param, params, extra_params={})
        key = param
        if @default_sort_key && @default_sort == param
          key = @default_sort_key
        else
          key += "_reverse" if params[:sort] == param
        end
        params = {:sort => key, 
                  :page => nil, 
                  :query => params[:query], 
                  :query_field => params[:query_field]}
        params.merge!(extra_params)

        options = {:action => action, 
                   :params => params}
        return options
      end
      
      def pagination_links(paginator, action, params, extra_params={})
        page_options = {:window_size => 1}
        pagination_links_each(paginator, page_options) do |n|
          params = {:sort => params['sort'], 
                    :page => n, 
                    :query => params[:query], 
                    :query_field => params[:query_field]}
          params.merge!(extra_params)
          
          link_to(n.to_s, {:action => action, :params => params})
        end
      end

      def value_provided?(params, name)
        !params[name].nil? && !params[name].empty?
      end
    end
end
